﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;

namespace OmniKits.Windows.Scripting
{
    using EssIL;
    using NullGuard;

    using Interop;
    using Interop.Interfaces;

    using EXCEPINFO = System.Runtime.InteropServices.ComTypes.EXCEPINFO;

    public class ActiveScriptHost<T> : ActiveScriptHost, ICloneable
        where T : class, new()
    {
        public ActiveScriptHost(bool useSafe = true)
            : this(new T(), useSafe)
        { }

        public ActiveScriptHost(T engine, bool useSafe = true)
            : base(engine, useSafe)
        { }

        private ActiveScriptHost(object engine, bool useSafe, bool initNew)
            : base(engine, useSafe, initNew)
        { }

        object ICloneable.Clone()
        {
            var newEngine = _IAS.Clone();

            var clone = new ActiveScriptHost<T>(newEngine, _SafeScript, false);
            clone.CopyItemsFrom(this);

            return clone;
        }

        public new ActiveScriptHost<T> Clone()
            => (ActiveScriptHost<T>)((ICloneable)this).Clone();
    }

    public class ActiveScriptHost
        : Core.DisposableBase, IActiveScriptSite, ICloneable
    {
        private struct ItemWithTypeInfo
        {
            public object Item;
            public Type Type;
            public ScriptItemFlag Flags;
        }

        private readonly Dictionary<string, ItemWithTypeInfo> _Items = new Dictionary<string, ItemWithTypeInfo>();

        private readonly object _Engine;
        private readonly ActiveScriptParseWrapper _Aspw;
        internal readonly IActiveScript _IAS;

        internal readonly bool _SafeScript = false;

        public static ActiveScriptHost Create(string engine, bool useSafe = true)
        {
            Guid clsid;
            if (Guid.TryParse(engine, out clsid))
                return new ActiveScriptHost(clsid, useSafe);
            else
                return new ActiveScriptHost(engine, useSafe);
        }

        public ActiveScriptHost(string progId, bool useSafe = true)
          : this(Activator.CreateInstance(Type.GetTypeFromProgID(progId)))
        { }

        public ActiveScriptHost(Guid clsid, bool useSafe = true)
          : this(Activator.CreateInstance(Type.GetTypeFromCLSID(clsid)))
        { }

        public ActiveScriptHost(object engine, bool useSafe = true)
          : this(engine, useSafe, true)
        { }

        internal ActiveScriptHost(object engine, bool useSafe, bool initNew)
        {
            _Engine = engine;

            if (_SafeScript = useSafe)
            {
                var ios = (IObjectSafety)_Engine;
                Guid riid = Guid.Empty;
                InterfaceSafeOption sup, old;

                ios.GetInterfaceSafetyOptions(ref riid, out sup, out old);

                if (!sup.Has(InterfaceSafeOption.UntrustedData))
                    throw new SecurityException();

                ios.SetInterfaceSafetyOptions(ref riid,
                  InterfaceSafeOption.UntrustedData,
                  InterfaceSafeOption.UntrustedData);
            }

            _Aspw = ActiveScriptParseWrapper.New(_Engine);

            if (initNew)
                _Aspw.InitNew();

            _IAS = _Engine as IActiveScript;

            _IAS.SetScriptSite(this);
            var guid = typeof(IActiveScriptSite).GUID;
            var hr = _IAS.GetScriptSite(ref guid);
        }

        public object GetScriptDispatch(string itemName = null)
            => _IAS.GetScriptDispatch(itemName);

        public void AddNamedItem(string name, ScriptItemFlag flags, object item)
        {
            ItemWithTypeInfo iwt = new ItemWithTypeInfo();
            iwt.Item = item;
            iwt.Type = item.GetType();
            iwt.Flags = flags;

            _Items.Add(name, iwt);
#if FixClone
            if (flags.Has(ScriptItemFlag.GlobalMembers))
                flags &= ~ScriptItemFlag.IsPersistent;
#endif
            _IAS.AddNamedItem(name, flags);
        }

        public void AddGlobalItem(object item, ScriptItemFlag flags = ScriptItemFlag.None)
        {
            AddNamedItem(Guid.NewGuid().ToString(), ScriptItemFlag.GlobalMembers | flags, item);
        }

        public void GetItemInfo(
            string name, ScriptInfoFlag returnMask,
            [AllowNull]object[] item, [AllowNull]IntPtr[] typeInfo)
        {
            ItemWithTypeInfo iwt = _Items[name];

            if (returnMask.Has(ScriptInfoFlag.IUnknown))
                item[0] = iwt.Item;

            if (returnMask.Has(ScriptInfoFlag.ITypeInfo))
                typeInfo[0] = Marshal.GetITypeInfoForType(iwt.Type);
        }

        public string AddScriptlet(
            string code, [AllowNull]string defaultName,
            ScriptTextFlag flags = ScriptTextFlag.None, uint start = 0,
            string itemName = null, string subItemName = null, string eventName = null)
        {
            string result = null;
            EXCEPINFO e;

            var hr = _Aspw.AddScriptlet(
                defaultName, code, itemName,
                subItemName, eventName,
                null, IntPtr.Zero,
                start, flags,
                out result, out e);
            if (hr.IsFailed())
                throw Marshal.GetExceptionForHR((int)hr);

            return result;
        }

        [DebuggerNonUserCode]
        public object ParseScriptText(string scriptBody, ScriptTextFlag flags = ScriptTextFlag.None, uint start = 0, string itemName = null)
        {
            object result = null;
            EXCEPINFO e;

            var hr = _Aspw.ParseScriptText(scriptBody, itemName, null, null, IntPtr.Zero, start, flags, out result, out e);
            if (HResult.SuccessOkay != hr)
                throw Marshal.GetExceptionForHR((int)hr);

            return result;
        }

        public object Eval(string expression)
            => ParseScriptText(expression, ScriptTextFlag.IsExpression);

        public void Run()
            => SetState(ScriptState.Started);

        public void SetState(ScriptState state)
            => _IAS.SetScriptState(state);

        string IActiveScriptSite.GetDocVersionString()
        {
            throw new NotImplementedException();
        }

        int IActiveScriptSite.GetLCID()
            => 0;
        void IActiveScriptSite.OnEnterScript()
        { }
        void IActiveScriptSite.OnLeaveScript()
        { }
        void IActiveScriptSite.OnScriptTerminate([AllowNull]object result, [AllowNull]EXCEPINFO[] exceptionInfo)
        { }
        void IActiveScriptSite.OnStateChange(ScriptState scriptState)
        { }
        void IActiveScriptSite.OnScriptError(IActiveScriptError scriptError)
        {
            uint context;
            uint line;
            int pos;
            scriptError.GetSourcePosition(out context, out line, out pos);

            var source = scriptError.GetSourceLineText();
            var ei = scriptError.GetExceptionInfo();
        }

        object ICloneable.Clone()
        {
            var newEngine = _IAS.Clone();

            var clone = new ActiveScriptHost(newEngine, _SafeScript, false);
            clone.CopyItemsFrom(this);

            return clone;
        }

        public ActiveScriptHost Clone()
            => (ActiveScriptHost)((ICloneable)this).Clone();

        public bool Close()
        {
            if (IsDisposeTriggered)
                return false;

            Dispose();
            return true;
        }

        protected void CopyItemsFrom(ActiveScriptHost source)
        {
            foreach (var item in source._Items)
            {
                var flags = item.Value.Flags;
                if (flags.Has(ScriptItemFlag.IsPersistent))
                {
#if FixClone
                    if (flags.Has(ScriptItemFlag.GlobalMembers))
                        AddNamedItem(item.Key, flags, item.Value.Item);
                    else
#endif
                        ((ICollection<KeyValuePair<string, ItemWithTypeInfo>>)_Items).Add(item);
                }
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                _IAS.Close();
            }
        }
    }
}
